﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
using SharePhone.Common.List;
using SharePhone.Core.Fields;

namespace SharePhone.Core
{
    /// <summary>
    /// Factory used to create core objects from SharePoint web service responses
    /// </summary>
    public class CoreObjectFactory
    {
        //private static ILog log = LogFactory.GetLogger(typeof(CoreObjectFactory));

        /// <summary>
        /// Creates a list of core objects based on a source collection. 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="source"></param>
        /// <returns></returns>
        public static BaseItemList<T> CreateCoreObjectList<T>(XElement source, SPList<T> parentList)
            where T : BaseItem, new()
        {
            if (source is XElement)
            {
                return CreateCoreObjectFromXml(source, parentList);
            }
            else
            {
                throw new BaseCoreException(source.GetType() + " is not a supported source type for conversion.");
            }
        }

        private static BaseItemList<T> CreateCoreObjectFromXml<T>(XElement item, SPList<T> parentList)
            where T : BaseItem, new()
        {
            XElement listitems = item.Element("{urn:schemas-microsoft-com:rowset}data");
            var list = new BaseItemList<T>();
            list.ParentList = parentList;
            if (listitems != null)
            {
                IEnumerable<XElement> itemList = from it in listitems.Elements() select it;
                Dictionary<string, CoreFieldAttribute> fields = GetItemFields<T>();
                foreach (XElement spItem in itemList)
                {
                    var o = CreateCoreObject<T>(fields, spItem);
                    o.LastRefreshTimeStamp = DateTime.Now;
                    o.ParentList = parentList;
                    list.Add(o);
                }
            }
            return list;
        }

        private static T CreateCoreObject<T>(Dictionary<string, CoreFieldAttribute> fields, XElement spItem)
            where T : BaseItem, new()
        {
            var o = new T();
            o.SPListItemId = Int32.Parse(spItem.Attribute("ows_ID").Value);
            o.UniqueId = new Guid(spItem.Attribute("ows_UniqueId").Value.Replace(o.SPListItemId + ";#", string.Empty));
            foreach (string fieldName in fields.Keys)
            {
                CoreFieldAttribute field = fields[fieldName];
                if (field.PopulateRule == CorePopulateRule.None)
                    continue;
                try
                {
                    string val = GetFieldValue(spItem, field);
                    PropertyInfo pInfo = o.GetType().GetProperty(fieldName);
                    pInfo.SetValue(o, PrepareValue(val, pInfo.PropertyType), null);
                }
                catch (NullReferenceException ex)
                {
                    if (field.Importance == FieldImportance.Required)
                        throw new BaseCoreException(
                            "Required field is missing a value or does not exist: " + field.FieldName, ex);
                }
            }
            return o;
        }

        private static string GetFieldValue(XElement spItem, CoreFieldAttribute field)
        {
            XAttribute att = spItem.Attribute("ows_" + field.FieldName);
            string val = null;
            if (att != null)
            {
                val = att.Value;
            }
            else if (field.DefaultValue != null && field.Importance == FieldImportance.Optional)
            {
                val = field.DefaultValue.ToString();
            }
            return val;
        }

        private static Dictionary<string, CoreFieldAttribute> GetItemFields<T>() where T : BaseItem
        {
            PropertyInfo[] props = typeof (T).GetProperties();
            return props.ToDictionary(pInfo => pInfo.Name, GetFieldAttributeFromProperty);
        }

        private static CoreFieldAttribute GetFieldAttributeFromProperty(PropertyInfo pInfo)
        {
            object[] attributes = pInfo.GetCustomAttributes(typeof (CoreFieldAttribute), false);
            if (attributes != null && attributes.Length > 0)
            {
                //if (pInfo.GetGetMethod().IsVirtual)
                return (CoreFieldAttribute) attributes[0];
                //else
                //    throw new BaseCoreException("Properties must be virtual");
            }
            return null;
        }

        /// <summary>
        /// For use with web service queries
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public static XElement GenerateCoreObjectViewFields<T>() where T : BaseItem
        {
            var parent = new XElement("ViewFields");
            var props = typeof (T).GetProperties();
            foreach (var fieldRef in from pInfo in props
                                     select pInfo.GetCustomAttributes(typeof (CoreFieldAttribute), false)
                                     into attributes where attributes != null && attributes.Length > 0 select (CoreFieldAttribute) attributes[0]
                                     into cfa where cfa.PopulateRule != CorePopulateRule.None select new XElement("FieldRef", new XAttribute("Name", cfa.FieldName)))
            {
                parent.Add(fieldRef);
            }
            return parent;
        }

        private static object ValidateValue(CoreFieldAttribute cfa, object value)
        {
            if (cfa.Importance == FieldImportance.Required &&
                (value == null || (value != null && value.ToString() == "")))
            {
                //log.Error("Required field is missing value: " + cfa.FieldName);
                throw new BaseCoreException("Required field is missing value: " + cfa.FieldName);
            }
            return value;
        }

        public static object PrepareValue(object value, Type conversionType)
        {
            if (value != null)
            {
                try
                {
                    if (conversionType.BaseType.FullName == "System.Enum")
                    {
                        //can't convert enums by usual method so we handle it here
                        return Enum.Parse(conversionType, value.ToString(), true);
                    }
                    switch (conversionType.Name)
                    {
                        case "SPLookupField":
                            return new SPLookupField(value as string);
                        case "Boolean":
                            return (value.ToString() == "1" ? true : false);
                        case "Int32":
                            return
                                Convert.ChangeType(
                                    Double.Parse(value.ToString(), CultureInfo.InvariantCulture.NumberFormat),
                                    conversionType, CultureInfo.InvariantCulture.NumberFormat);
                    }
                    if (value.ToString().IndexOf(";#") != -1)
                    {
                        var lookup = new SPLookupField(value as string);
                        value = lookup.LookupValue;
                    }
                    return Convert.ChangeType(value, conversionType, CultureInfo.InvariantCulture);
                }
                catch (InvalidCastException)
                {
                    throw;
                }
            }
            else
            {
                return null;
            }
        }

        /// <summary>
        /// Converts a value to a type using the .Parse() method.
        /// </summary>
        /// <param name="type"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        private static object TypeParse(Type type, object value)
        {
            try
            {
                MethodInfo mi = type.GetMethod("Parse", new[] {typeof (String)});
                if (mi != null && value != null)
                {
                    object o = type.InvokeMember("Parse", BindingFlags.InvokeMethod, null, type, new[] {value});
                    return o;
                }
                return value;
            }
            catch (Exception ex)
            {
                throw new BaseCoreException("Error while trying to parse value into type " + type + ": " + ex.Message,
                                            ex);
            }
        }

        /// <summary>
        /// Returns a property value using reflection
        /// </summary>
        /// <param name="o"></param>
        /// <param name="propertyName"></param>
        /// <returns></returns>
        internal static object GetPropertyValue(object o, string propertyName)
        {
            try
            {
                var pInfo = o.GetType().GetProperty(propertyName);
                return pInfo.GetValue(o, null);
            }
            catch (Exception)
            {
                //log.Warn(String.Format("Error retrieving property value from {0}: {1}. Returning NULL as value and assuming the property isn't to be used.", propertyName, ex.Message), ex);
                return null;
            }
        }
    }
}